package com.roadmap.client.module.plan;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;

import com.roadmap.client.common.ISimpleResultSet;
import com.roadmap.client.module.task.ITaskProgress;
import com.roadmap.client.module.task.TaskProgress;
import com.roadmap.common.BaseConst;
import com.roadmap.common.BaseUtility;
import com.roadmap.common.BaseEnum.TimeType;
import com.roadmap.common.RdcLogger;
import com.roadmap.entities.LifePlan;
import com.roadmap.entities.LifeTask;
import com.roadmap.entities.Milestone;
import com.roadmap.exception.InvalidQueryException;
import com.roadmap.exception.InvalidValueException;
import com.roadmap.exception.NullObjectException;
import com.roadmap.exception.ServerException;
import com.roadmap.repo.db.DatabaseUtility;
import com.roadmap.repo.db.IDatabaseUtility;

public class PlanProgress implements IPlanProgress {
	private final static Logger logger = RdcLogger
			.getLogger(PlanProgress.class);
	private final IDatabaseUtility dbUtil;
	private ITaskProgress taskPro = new TaskProgress();

	public PlanProgress() {
		this.dbUtil = new DatabaseUtility();
	}

	public PlanProgress(IDatabaseUtility dbUtil) {
		this.dbUtil = dbUtil;
	}

	/**
	 * prePro=前一个已达到milestone应完成的预定进度量;
	 * proDiff=前一个已达到的milestone与下一个未达到milestone之间的进度量;
	 * dayDiff=前一个已达到的milestone与下一个未达到milestone之间的天数; curPro/curDays，即每天预定完成计划量；
	 * curDays=当前时间相对前一个已达到的milestone的天数；
	 * 理想的进度=prePro+(proDiff/dayDiff)*curDays;
	 */
	public int getIdealProgress(LifePlan plan) throws NullObjectException,
			InvalidQueryException, InvalidValueException, ServerException {
		Date now = new Date();
		List<Milestone> milestones = getAllMilestonesOfPlan(plan);
		// if the today happens to equal to one milestone, straightly return
		// milestone's proportion of its plan
		for (int count = 0; count < milestones.size(); count++) {
			Milestone ms = milestones.get(count);
			Date stoneDate = ms.getStoneDate();
			int diff = BaseUtility.getTimeDiffBetween(now, stoneDate,
					TimeType.DAY);
			if (diff == 0) {
				return ms.getProportionOfPlan();
			}
		}

		List<Integer> daysBetweeStones = getDaysBetweenNeighborStones(plan,
				milestones);
		assert (daysBetweeStones.size() == milestones.size());
		// before the first milestone
		int prePro = 0;
		Date preDate = plan.getScheduledStartDate();
		for (int count = 0; count < milestones.size(); count++) {
			Milestone ms = milestones.get(count);
			if (ms.getStatus() == BaseConst.MILESTONE_STATUS_SATISFIED) {
				prePro = ms.getProportionOfPlan();
				preDate = ms.getStoneDate();
				continue;
			}
			int proDiff = ms.getProportionOfPlan() - prePro;
			int dayDiff = daysBetweeStones.get(count);
			if (dayDiff < 0) {
				break;
			}
			else if (dayDiff == 0) {
				// suppose two milestones should be satisfied at one day
				dayDiff++;
			}

			// String msg = "previous stone's proportion: " + prePro;
			// logger.debug(msg);
			// msg = "the diff between unsatisfied and last completed one: " +
			// proDiff;
			// logger.debug(msg);
			// msg = "the days between unsatisfied and last completed one: " +
			// dayDiff;
			// logger.debug(msg);

			int curDays = BaseUtility.getTimeDiffBetween(now, preDate,
					TimeType.DAY);
			if (curDays < 0) {
				InvalidValueException ive = new InvalidValueException(
						"the milestone was completed in a future time which is invalid.");
				logger.error(ive);
				throw ive;
			}
			return (int) (prePro + (1.0f * proDiff * curDays) / dayDiff);
		}
		return 0;
	}

	/**
	 * the actual progress equals to the proportion which all tasks of the plan
	 * complete
	 */
	public int getActualProgress(LifePlan plan) throws NullObjectException,
			InvalidQueryException, InvalidValueException, ServerException {
		if (plan == null || plan.getId() == null) {
			NullObjectException noe = new NullObjectException(
					"the entity can't be NULL");
			logger.error(noe);
			throw noe;
		}

		StringBuffer criteria = new StringBuffer("planId='");
		criteria.append(plan.getId());
		criteria.append("' and status<>");
		criteria.append(BaseConst.LIFETASK_STATUS_DELETED);
		List tasks = dbUtil.getEntitiesByCriteria(LifeTask.class, criteria
				.toString());

		int total = 0;
		Iterator iter = tasks.iterator();
		while (iter.hasNext()) {
			LifeTask task = (LifeTask) iter.next();
			total += taskPro.getActualProgressInPlan(task);
		}

		return total;
	}

	/**
	 * @param srs:
	 *            this set contains IDs of plan's tasks and their corresponding
	 *            actual progress
	 */
	public int getActualProgress(LifePlan plan, ISimpleResultSet srs)
			throws NullObjectException, InvalidQueryException,
			InvalidValueException, ServerException {
		if (plan == null || plan.getId() == null || srs == null) {
			NullObjectException noe = new NullObjectException(
					"the entity can't be NULL");
			logger.error(noe);
			throw noe;
		}

		StringBuffer criteria = new StringBuffer("planId='");
		criteria.append(plan.getId());
		criteria.append("' and status<>");
		criteria.append(BaseConst.LIFETASK_STATUS_DELETED);
		List tasks = dbUtil.getEntitiesByCriteria(LifeTask.class, criteria
				.toString());

		ITaskProgress taskPro = new TaskProgress();
		int total = 0;
		Iterator iter = tasks.iterator();
		while (iter.hasNext()) {
			LifeTask task = (LifeTask) iter.next();
			int pro = 0;
			try {
				if (srs.hasKey(task.getId()))
					pro = (Integer) srs.get(task.getId());
				else {
					pro = taskPro.getActualProgressInPlan(task);
					srs.add(task.getId(), pro);
				}
			}
			catch (Exception ex) {
				logger.error(ex);
				pro = taskPro.getActualProgressInPlan(task);
				srs.add(task.getId(), pro);
			}
			finally {
				total += pro;
			}
		}

		return total;
	}

	/**
	 * get all milestones of a plan. a plan has at least one Virtual milestone.
	 * The virtual milestones is the start of the plan. This one will not be
	 * stored in database. The milestones stored in database are called Real
	 * ones.
	 */
	@SuppressWarnings("unchecked")
	public List<Milestone> getAllMilestonesOfPlan(LifePlan plan)
			throws NullObjectException, InvalidQueryException,
			InvalidValueException, ServerException {
		if (plan == null || plan.getId() == null) {
			NullObjectException noe = new NullObjectException(
					"the entity can't be NULL");
			logger.error(noe);
			throw noe;
		}

		List<Milestone> milestones = new ArrayList<Milestone>();

		// the returned milestones will be ordered by proporationOfPlan from
		// small to big. when a big milestone is satisfied, the ones smaller
		// than it must be satisfied, too
		StringBuffer criteria = new StringBuffer("planId='");
		criteria.append(plan.getId());
		criteria.append("' order by proportionOfPlan ASC");
		List<Milestone> list = (List<Milestone>) dbUtil.getEntitiesByCriteria(
				Milestone.class, criteria.toString());
		if (list != null && list.size() > 0)
			milestones.addAll(list);

		// once a plan is created, it already has one milestone which are
		// the start of the plan
		Milestone last = new Milestone();
		last.setPlanId(plan.getAspirantId());
		last.setProportionOfPlan(100);
		last.setStoneDate(plan.getScheduledEndDate());
		milestones.add(last);

		return milestones;
	}

	/** get the days between two neighbor milestones */
	private List<Integer> getDaysBetweenNeighborStones(LifePlan plan,
			List<Milestone> milestones) throws NullObjectException,
			InvalidValueException {
		if (plan == null || milestones == null || milestones.size() <= 0) {
			NullObjectException noe = new NullObjectException(
					"the entity can't be NULL");
			logger.error(noe);
			throw noe;
		}

		Date preDate = plan.getScheduledStartDate();
		Iterator iter = milestones.iterator();
		List<Integer> daysBetweeStones = new ArrayList<Integer>();
		while (iter.hasNext()) {
			Milestone ms = (Milestone) iter.next();
			int days = BaseUtility.getTimeDiffBetween(ms.getStoneDate(),
					preDate, TimeType.DAY);
			if (days < 0) {
				InvalidValueException ive = new InvalidValueException(
						"the big milestone's target date is smaller than the previous one which is invalid.");
				logger.error(ive);
				throw ive;
			}
			daysBetweeStones.add(new Integer(days));
			preDate = ms.getStoneDate();

			if (logger.isDebugEnabled()) {
				logger
						.debug("day diff between two neighbor milestones:"
								+ days);
			}
		}

		return daysBetweeStones;
	}

	public int getAssignedProportion(String planId) throws NullObjectException,
			InvalidQueryException, ServerException {
		if (planId == null || planId.trim().equals("")) {
			NullObjectException noe = new NullObjectException(
					"the entity can't be NULL");
			logger.error(noe);
			throw noe;
		}

		StringBuffer sql = new StringBuffer(
				"select sum(proportionOfPlan) from lifeTask where planId='");
		sql.append(planId);
		sql.append("' and status<>");
		sql.append(BaseConst.LIFETASK_STATUS_DELETED);
		List list = dbUtil.queryBySQL(sql.toString());
		if (list != null && list.size() > 0) {
			Object obj = list.get(0);
			if (obj != null){
				return ((BigDecimal) obj).intValue();
			}
		}
		return 0;
	}

}
